Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 628ef907a8a1c32dd5f0c2bd186f480b5e46bab4


Parents : 037d3f2
Author : Mark Qvist <bc7291552be7a58f361522990465165c>
Signature : T66BB85Valid, signed by author
Date : 2026-07-25T15:35:58+02:00

Added normal output / loudspeaker switching to telephony primitive

Changes

7 files changed, 46 insertions(+), 6 deletions(-)

M setup.py +2 -2

Diff

diff --git a/LXST/Filters.py b/LXST/Filters.py
index da0aaca..80b7453 100644
--- a/LXST/Filters.py
+++ b/LXST/Filters.py
@@ -869,7 +869,9 @@ class EchoSuppressor(Filter):
if corr > self.corr_threshold: self._last_echo_correlation = time.time()
echo_correlated = now < self._last_echo_correlation + self.echo_correlation_window
- if mic_energy > 2e-6: self.ser_db = 10.0 * np.log10(mic_energy/predicted_echo_energy + 1e-12)
+ if mic_energy > 2e-6:
+ try: self.ser_db = 10.0 * np.log10(mic_energy/predicted_echo_energy + 1e-12)
+ except Exception as e: RNS.log(f"Numerical error in energy calculation: {e}", RNS.LOG_DEBUG)
# Near-end energy estimate (residual after predicted echo)
near_energy = max(0.0, mic_energy - predicted_echo_energy)

diff --git a/LXST/Mixer.py b/LXST/Mixer.py
index 27335ca..3326cb3 100644
--- a/LXST/Mixer.py
+++ b/LXST/Mixer.py
@@ -102,6 +102,7 @@ class Mixer(LocalSource, LocalSink):
else: return 10**(self.gain/10)
def _mixer_job(self):
+ if self.mixer_lock.locked(): return
with self.mixer_lock:
while self.should_run:
if self.sink and self.sink.can_receive():
@@ -135,6 +136,8 @@ class Mixer(LocalSource, LocalSink):
else: time.sleep(self.frame_time*0.1)
+ RNS.log(f"{self} stopped", RNS.LOG_DEBUG)
+
@property
def codec(self):
return self._codec

diff --git a/LXST/Pipeline.py b/LXST/Pipeline.py
index 4d2a0b7..02681a1 100644
--- a/LXST/Pipeline.py
+++ b/LXST/Pipeline.py
@@ -26,6 +26,7 @@ class Pipeline():
if isinstance(sink, Loopback): sink.samplerate = source.samplerate
if isinstance(source, Loopback): source._sink = sink
+ if isinstance(source, Mixer): source._sink = sink
if isinstance(sink, Packetizer): sink.source = source
if isinstance(sink, OpusFileSink): sink.source = source

diff --git a/LXST/Platforms/linux/soundcard.py b/LXST/Platforms/linux/soundcard.py
index 7f11123..666436d 100644
--- a/LXST/Platforms/linux/soundcard.py
+++ b/LXST/Platforms/linux/soundcard.py
@@ -231,7 +231,7 @@ class _PulseAudio:
info.append(info_dict)
self._pa_context_get_source_info_by_name(self.context, id.encode(), callback, _ffi.NULL)
- return info[0]
+ return info[0] if len(info) else None
@property
def sink_list(self):

diff --git a/LXST/Primitives/Telephony.py b/LXST/Primitives/Telephony.py
index 52f1daf..5fa698c 100644
--- a/LXST/Primitives/Telephony.py
+++ b/LXST/Primitives/Telephony.py
@@ -216,11 +216,13 @@ class Telephone(SignallingReceiver):
self.transmit_pipeline = None
self.__receive_muted = False
self.__transmit_muted = False
+ self.__loudspeaker_on = False
self.ringer_lock = threading.Lock()
self.ringer_output = None
self.ringer_pipeline = None
self.ringtone_path = None
self.speaker_device = None
+ self.loudspeaker_device = None
self.microphone_device = None
self.ringer_device = None
self.low_latency_output = False
@@ -279,6 +281,10 @@ class Telephone(SignallingReceiver):
self.speaker_device = device
RNS.log(f"{self} speaker device set to {device}", RNS.LOG_DEBUG)
+ def set_loudspeaker(self, device):
+ self.loudspeaker_device = device
+ RNS.log(f"{self} loudspeaker device set to {device}", RNS.LOG_DEBUG)
+
def set_microphone(self, device):
self.microphone_device = device
RNS.log(f"{self} microphone device set to {device}", RNS.LOG_DEBUG)
@@ -459,6 +465,10 @@ class Telephone(SignallingReceiver):
if not self.transmit_mixer: return False
else: return self.transmit_mixer.muted
+ @property
+ def loudspeaker_on(self):
+ return self.__loudspeaker_on
+
def signal(self, signals, link):
if type(signals) != list: signals = [signals]
for signal in signals:
@@ -509,6 +519,7 @@ class Telephone(SignallingReceiver):
self.call_status = Signalling.STATUS_AVAILABLE
self.__receive_muted = False
self.__transmit_muted = False
+ self.__loudspeaker_on = False
if remote_identity: RNS.log(f"Call with {RNS.prettyhexrep(remote_identity.hash)} terminated", RNS.LOG_DEBUG)
else: RNS.log(f"Outgoing call could not be connected, link establishment failed", RNS.LOG_DEBUG)
@@ -537,6 +548,16 @@ class Telephone(SignallingReceiver):
self.__transmit_muted = not unmute
if self.transmit_mixer: self.transmit_mixer.unmute(unmute)
+ def enable_loudspeaker(self, enable=True):
+ was_on = self.__loudspeaker_on
+ self.__loudspeaker_on = enable
+ if was_on != self.__loudspeaker_on: self.__update_audio_output()
+
+ def disable_loudspeaker(self, disable=True):
+ was_on = self.__loudspeaker_on
+ self.__loudspeaker_on = not disable
+ if was_on != self.__loudspeaker_on: self.__update_audio_output()
+
def squelch_transmit(self, squelch=True):
if self.active_call and hasattr(self.active_call, "packetizer"):
if squelch: self.active_call.packetizer.squelch()
@@ -619,11 +640,24 @@ class Telephone(SignallingReceiver):
def __prepare_dialling_pipelines(self):
self.__select_call_profile(self.active_call.profile)
self.__select_call_mode(self.active_call.call_mode)
- if self.audio_output == None: self.audio_output = LineSink(preferred_device=self.speaker_device)
+ if self.audio_output == None: self.audio_output = LineSink(preferred_device=self.speaker_device) if not self.__loudspeaker_on else LineSink(preferred_device=self.loudspeaker_device)
if self.receive_mixer == None: self.receive_mixer = Mixer(target_frame_ms=self.target_frame_time_ms, gain=self.receive_gain)
if self.dial_tone == None: self.dial_tone = ToneSource(frequency=self.dial_tone_frequency, gain=0.0, ease_time_ms=self.dial_tone_ease_ms, target_frame_ms=self.target_frame_time_ms, codec=Null(), sink=self.receive_mixer)
if self.receive_pipeline == None: self.receive_pipeline = Pipeline(source=self.receive_mixer, codec=Null(), sink=self.audio_output)
+ def __update_audio_output(self):
+ if self.audio_output and self.receive_pipeline:
+ with self.pipeline_lock:
+ self.receive_mixer.dbgout = True
+ previous_output = self.audio_output
+ previous_pipeline = self.receive_pipeline
+ self.audio_output = LineSink(preferred_device=self.speaker_device) if not self.__loudspeaker_on else LineSink(preferred_device=self.loudspeaker_device)
+ self.receive_pipeline = Pipeline(source=self.receive_mixer, codec=Null(), sink=self.audio_output)
+ self.receive_pipeline.start()
+ previous_pipeline.stop()
+ previous_output.stop()
+ self.receive_mixer.start()
+
def __activate_ring_tone(self):
if self.ringtone_path != None and os.path.isfile(self.ringtone_path):
if not self.ringer_pipeline:

diff --git a/LXST/_version.py b/LXST/_version.py
index 3d18726..dd9b22c 100644
--- a/LXST/_version.py
+++ b/LXST/_version.py
@@ -1 +1 @@
-__version__ = "0.5.0"
+__version__ = "0.5.1"

diff --git a/setup.py b/setup.py
index ba5e50d..1aabef6 100644
--- a/setup.py
+++ b/setup.py
@@ -70,8 +70,8 @@ setuptools.setup(
'rnphone=LXST.Utilities.rnphone:main',
]
},
- install_requires=["rns>=1.3.8",
- "lxmf>=1.0.1",
+ install_requires=["rns>=1.4.1",
+ "lxmf>=1.1.0",
"numpy>=2.3.4",
"pycodec2>=4.1.0",
"audioop-lts>=0.2.1;python_version>='3.13'",


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────